/*
 * DG (DG Gaming Engine)  - Server
 * Copyright (C) 2004  Bob Marks (marksie531@yahoo.com)
 * http://code.google.com/p/dng-gaming
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package com.dg.gaming.server.controllers;

import java.util.*;

import com.dg.gaming.api.common.*;
import com.dg.gaming.api.common.User;
import com.dg.gaming.api.common.msg.*;
import com.dg.gaming.api.common.playerstate.*;
import com.dg.gaming.server.*;
import com.dg.gaming.server.data.*;
import nanoxml.*;

/**
 * Class which parses table communication messages (messages with a
 * "table" attribute).
 *
 * @author  Bob Marks
 * @version Beta 0.3
 */
public class ServerTableController {

    /** Convenience link to the server. */
    protected GameServer server;

    /**
     * Constructor of a ServerTable parser.
     */
    public ServerTableController() {
        // Set up fields
        this.server = GameServer.getInstance();
    }

    /**
     * This implements parsing of a table message.
     *
     * @param message
     */
    public synchronized void parseTableMessage(ServerConnectionThread conn,
                                               XMLElement message,
                                               String tableNum) {
        // First, make sure that the given table number is valid.
        // (Doing this once here means that all of the routines below can assume
        //  that the table is valid.)
        Table table = conn.getTableList().getTable(tableNum);
        if (table == null) {
            return;
        }

        // Retrieve the message type
        String messageType = message.getName();

        try {
            // Parse the message
            if (messageType.equals(Msg.CONNECT_TABLE)) {

            } else if (messageType.equals(Msg.CHAT_TABLE)) {
                chatTable(conn, new MsgChatTable(message), tableNum);
            } else if (messageType.equals(Msg.JOIN_TABLE)) {
                joinTable(conn, new MsgJoinTable(message), tableNum, table);
            } else if (messageType.equals(Msg.SIT_DOWN)) {
                sitDown(conn, new MsgSitDown(message), tableNum, table);
            } else if (messageType.equals(Msg.STAND_UP)) {
                standUp(conn, new MsgStandUp(message), tableNum, table);
            } else if (messageType.equals(Msg.READY_TO_START)) {
                readyToStart(conn, new MsgReadyToStart(message), tableNum,
                             table);
            } else if (messageType.equals(Msg.NEXT_PLAYER)) {
                nextPlayer(conn, new MsgNextPlayer(message), tableNum, table);
            } else if (messageType.equals(Msg.EXIT_TABLE)) {
                exitTable(conn, new MsgExitTable(message), tableNum);
            } else if (messageType.equals(Msg.OFFER_DRAW)) {
                offerDraw(conn, new MsgOfferDraw(message), tableNum, table);
            } else if (messageType.equals(Msg.GAME_OVER)) {
                gameOver(conn, new MsgGameOver(message), tableNum, table);
            } else if (messageType.equals(Msg.CONTROLLER_PROPERTY)) {
                controllerProperty(conn, new MsgControllerProperty(message),
                                   tableNum);
            } else if (messageType.equals(Msg.CONTROLLER_OBJECT)) {
                controllerObject(conn, new MsgControllerObject(message),
                                 tableNum);
            } else if (messageType.equals(Msg.TABLE_PROPERTY)) {
                tableProperty(conn, new MsgTableProperty(message), table);
            }

            // Delegate to the correct controller to do a additional processing
            String gameID = conn.getGameID();
            if (gameID != null) {
                ServerController controller = conn.getServerController();
                if (controller != null) {
//                    controller.parseTableMessage(conn, message, tableNum);
                }
            }
        } catch (TransmissionException transEx) {
            transEx.printStackTrace();
        } catch (ServerDataException sdEx) {
            conn.serverSend(new MsgError(IError.SERVER_DATA_ERROR,
                                         sdEx.getMessage()));
        }
    }


    private void tableConnect(ServerConnectionThread conn,
                              MsgConnectTable tblConnect, String tableNum) throws
            TransmissionException {


    }


    /**
     * Transmit a message to everyone in the room except the person who sent it
     * in the first place.
     *
     * @param conn        Connection back to the client.
     * @param commMessage Communications message.
     * @param tableNum    Table number.
     * @throws TransmissionException
     */
    private void chatTable(ServerConnectionThread conn,
                           MsgChatTable commMessage, String tableNum) throws
            TransmissionException {
        // Broadcast to all users in a room (in this case a table)
        commMessage.setUsername(conn.getUserId());

        // Retrieve table and then player list from the table
        conn.transmitToTablePlayers(conn.getUserId(), tableNum, commMessage);
    }

    /**
     * A client is sending a property to the server.  Update all the other clients
     * at the the table with this property.
     *
     * @param conn          Connection back to the client.
     * @param commContProp  Communications message.
     * @param tableNum      Table number.
     * @throws TransmissionException
     */
    private void controllerProperty(ServerConnectionThread conn,
                                    MsgControllerProperty commContProp,
                                    String tableNum) throws
            TransmissionException {
        // Transmit to other players
        commContProp.setUsername(conn.getUserId());
        conn.transmitToTablePlayers(conn.getUserId(), tableNum, commContProp);

        // And also inform server controller
        ServerController controller = conn.getServerController();
        if (controller != null) {

            // Retrieve and check that model isn't null
            GameModel model = controller.getModel(tableNum);
            if (model != null) {

                // Depending on type of property delegate to the correct adapter method
                switch (commContProp.getStatus()) {
                case MsgControllerProperty.TYPE_STRING:
                    controller.receiveProperty(model, commContProp.getKey(),
                                               commContProp.getValue());
                    return;

                case MsgControllerProperty.TYPE_INT:
                    int value = Integer.parseInt(commContProp.getValue());
                    controller.receiveProperty(model, commContProp.getKey(),
                                               value);
                    return;

                case MsgControllerProperty.TYPE_INT_TWO:
                    StringTokenizer st = new StringTokenizer(commContProp.
                            getValue());
                    int value1 = Integer.parseInt(st.nextToken());
                    int value2 = Integer.parseInt(st.nextToken());
                    controller.receiveProperty(model, commContProp.getKey(),
                                               value1, value2);
                    return;
                }
            }
        }
    }

    /**
     * A client is sending an object to the server. Update all other clients
     * at a table with this property.
     *
     * @param conn          Connection back to the client.
     * @param commContObj   Communications message.
     * @param tableNum      Table number.
     * @throws TransmissionException
     */
    private void controllerObject(ServerConnectionThread conn,
                                  MsgControllerObject commContObj,
                                  String tableNum) throws TransmissionException {
        // Transmit to other players
        commContObj.setUsername(conn.getUserId());
        conn.transmitToTablePlayers(conn.getUserId(), tableNum, commContObj);

        // And send to server controller
        ServerController controller = conn.getServerController();
        if (controller != null) {

            // Retrieve and check that model isn't null
            GameModel model = controller.getModel(tableNum);
            if (model != null) {
                controller.receiveObject(model, commContObj.getData(), tableNum);
            }
        }
    }

    /**
     * Send a table property to everyone.
     *
     * @param conn
     * @param commTableProperty
     * @param table              The table.
     */
    private void tableProperty(ServerConnectionThread conn,
                               MsgTableProperty commTableProperty, Table table) {
        // Update table with key / value
        String key = commTableProperty.getKey();
        String value = commTableProperty.getValue();
        table.addProperty(key, value);

        conn.broadcast(commTableProperty);
        conn.sendDataMessageToAdmin(commTableProperty);
    }

    /**
     * The new way of performing a game over is that a client MAY inform a server
     * that a game is over if the game over detection requires a lot of
     * processing e.g. detecting chess game overs.  This means the client tries
     * to detect if its game over after each move and the only time the server
     * needs to check is if the client says its game over.  This enables the
     * server to be more efficient.
     *
     * @param conn          Connection to client.
     * @param commGameOver  GameOver object.
     * @param tableNum      Table number.
     * @param table         The table.
     */
    private void gameOver(ServerConnectionThread conn,
                          MsgGameOver commGameOver, String tableNum,
                          Table table) {
        // Ensure that the player sending this message is actually playing the game.
        long msgPlayerName = conn.getUserId();
        if (!table.isParticipatingPlayer(msgPlayerName)) {
            return;
        }

        // Inform server controller that the client says the game is over.
        // The server should perform its own check to ensure this is correct
        ServerController controller = conn.getServerController();
        if (controller != null) {
            // Check status.  It should either be user / resign
            int status = commGameOver.getStatus();
            switch (status) {
            case IGameOver.USER_RESIGNS:
//                controller.userResigns(conn, tableNum);
                break;
            default:
//                controller.gameOver(conn, tableNum, status);
                break;
            }
        }
    }

    /**
     * Transmit offer draw communications object.
     *
     * @param conn           Connection back to the client.
     * @param commOfferDraw  Communications message.
     * @param tableNum       Table number.
     * @param table          The table.
     */
    private void offerDraw(ServerConnectionThread conn,
                           MsgOfferDraw commOfferDraw, String tableNum,
                           Table table) {
        // Ensure that the player sending this message is actually playing the game.
        long msgPlayerName = conn.getUserId();
        Player player = table.getPlayerList().getPlayer(msgPlayerName);
        if ((player == null) ||
            (player.getSeatNum() == Player.NOT_SEATED)) {
            return;
        }

        // Parse the message
        int status = commOfferDraw.getStatus();
        // Message can be accept, decline or request
        if (status == MsgInvite.REQUEST) {
            if (table.hasOutstandingDrawOffer()) {
                // If this table has an outstanding draw offer, then take this
                // request for draw as an accept (and do the code below for accept.)
                status = MsgInvite.ACCEPT;
            } else {
                // This table doesn't have an outstanding draw offer, so start
                // one, with this player being the instigator
                int offerSerialNum = table.startDrawOffer(player);

                // Send the message to everyone else to ask them for response.
                commOfferDraw.setSerialNum(offerSerialNum);
                commOfferDraw.setUsername(msgPlayerName);
                conn.transmitToTablePlayers(msgPlayerName, tableNum,
                                            commOfferDraw, true);
            }
        }

        if (status == MsgInvite.ACCEPT) {
            table.acceptDrawOffer(player, commOfferDraw.getSerialNum());

            if (table.drawOfferComplete()) {
                // Clear the draw offer from the table
                table.clearDrawOffer();

                // Delegate this call to the server controller
                ServerController controller = conn.getServerController();
                if (controller != null) {
//                    controller.usersAgreeDraw(conn, tableNum);
                }
            }
        } else if (status == MsgInvite.DECLINE) {
            if (table.declineDrawOffer(player, commOfferDraw.getSerialNum())) {
                // Clear the draw offer from the table
                table.clearDrawOffer();

                // Tell everyone that this player declined the draw offer
                commOfferDraw.setUsername(msgPlayerName);
                conn.transmitToTablePlayers(msgPlayerName, tableNum,
                                            commOfferDraw);
            }
        }
    }

    /**
     * Transmit a next player request to users.
     *
     * @param conn            Connection to client.
     * @param commNextPlayer  Next player message.
     * @param tableNum        Table number.
     * @param table           The table.
     */
    private void nextPlayer(ServerConnectionThread conn,
                            MsgNextPlayer commNextPlayer, String tableNum,
                            Table table) {
        // Advance to the next player
        table.nextPlayer();
        XMLElement chiled = commNextPlayer.getChiled();
        if (chiled != null) {
            chiled.setAttribute(MsgTableMessage.XML_ATT_TABLE_NUM, tableNum);
            parseTableMessage(conn, chiled, tableNum);
        }
        // Set the username of the next player and transmit this to players at table
        commNextPlayer.setUsername(table.getPlayerList().getCurrentPlayerId());
        conn.transmitToTablePlayers(tableNum, commNextPlayer);
        conn.sendDataMessageToAdmin(commNextPlayer);
    }

    /**
     * Transmit exit table message.,
     *
     * @param conn           Connection to client.
     * @param commExitTable  Exit table communication message.
     * @param tableNum       Table number.
     */
    private void exitTable(ServerConnectionThread conn,
                           MsgExitTable commExitTable, String tableNum) throws
            ServerDataException {
        // Retrieve table list
        TableList tableList = conn.getTableList();
        long username = conn.getUserId();
        int numOfTables = tableList.size();

        // Make sure the player was removed before sending out messages
        if (tableList.removePlayer(tableNum, username)) {

            // Broadcast to all user and admin
            MsgExitTable commExitReply = new MsgExitTable(tableNum, username);
            conn.broadcast(username, commExitReply); // inform all users
            conn.sendDataMessageToAdmin(commExitReply);

            // Update snapshot as a table may have been removed
            if (tableList.size() != numOfTables) {
                server.getServerData().updateSnapshot(
                        conn.getGameID(),
                        conn.getUserList().size(),
                        conn.getTableList().size());
            }
        }
    }

    /**
     * User joins a table.
     *
     * @param conn            Connection to client.
     * @param commJoinTable   CommJoinTable communication message from client.
     * @param tableNum        Table number we are interested in.
     * @param table           The table we are interested in.
     */
    private void joinTable(ServerConnectionThread conn,
                           MsgJoinTable commJoinTable, String tableNum,
                           Table table) {
        // Unpack join table message from XML
        long username = conn.getUserId();
        User newUser = conn.getUserList().getUser(username);

        // Make sure the player joined successfully before sending out replies
        if (table.addPlayer(newUser)) {

            // Create response join table message
            // The user who is joining also gets the player list
            commJoinTable = new MsgJoinTable(username, table);
            conn.transmit(username, commJoinTable);

            // set back to normal again and broadcast the table object to all users
            // omiting this user (as they already have it) and to admin.
            commJoinTable = new MsgJoinTable(username, tableNum);
            conn.broadcast(username, commJoinTable);
            conn.sendDataMessageToAdmin(commJoinTable);
        }
    }

    /**
     * User stands up at a table.
     *
     * @param conn         Connection to the server.
     * @param commStandUp  CommStandUp message.
     * @param tableNum     The table number we're standing up from.
     * @param table        The table we're standing up from.
     */
    private void standUp(ServerConnectionThread conn, MsgStandUp commStandUp,
                         String tableNum, Table table) {
        // Get the player
        Player player = table.getPlayerList().getPlayer(conn.getUserId());
        if (player == null) {
            return;
        }

        // Change the player's state
        player.stand();

        // Transmit to all players at table & admin that this user has sat down
        MsgPlayerState commPlayerState = new MsgPlayerState(
                tableNum, conn.getUserId(), player.getState());
        conn.transmitToTablePlayers(tableNum, commPlayerState);
        conn.sendDataMessageToAdmin(commPlayerState);
    }

    /**
     * Ready to start.
     *
     * @param conn         Connection to client.
     * @param commReady    Ready to start communications message.
     * @param tableNum     Table number.
     * @param table        The table
     */
    private void readyToStart(ServerConnectionThread conn,
                              MsgReadyToStart commReady, String tableNum,
                              Table table) {
        // Get the player
        PlayerList players = table.getPlayerList();
        Player player = players.getPlayer(conn.getUserId());
        if (player == null) {
            return;
        }

        // Change the player's state
        player.start();

        // Transmit state to table players & admin
        MsgPlayerState commPlayerState = new MsgPlayerState(
                tableNum, conn.getUserId(), player.getState());
        conn.transmitToTablePlayers(tableNum, commPlayerState);
        conn.sendDataMessageToAdmin(commPlayerState);

        // Check if this game is to start
        int numOfPlayers = table.getNumOfPlayers();
        int count = players.getPlayerStateCount(PlayerState.READY_TO_START);

        if (count == numOfPlayers) {

            // Update all the users to have the status of started and
            // inform everyone at the table
            Vector playersVector = players.getPlayersSortedBySeat(PlayerState.
                    READY_TO_START);
            for (int i = 0; i < playersVector.size(); i++) {
                Player curPlayer = (Player) playersVector.get(i);
                curPlayer.start();

                // Transmit "start" state to table players
                commPlayerState = new MsgPlayerState(
                        tableNum, curPlayer.getPlayerId(), curPlayer.getState());
                conn.transmitToTablePlayer(curPlayer.getPlayerId(), tableNum,
                                           commPlayerState);

                conn.sendDataMessageToAdmin(commPlayerState);
            }

            // Reset the current player
            players.resetCurrentPlayer();

            // Inform the server controller if it exists
            ServerController controller = conn.getServerController();
            if (controller != null) {
                controller.startGame(tableNum);
            }

            // Send individual game start messages to all players
            MsgStartGame commStartGame = new MsgStartGame(players.
                    getCurrentPlayerId(), tableNum);
            conn.transmitToTablePlayers(tableNum, commStartGame);

            // Let the game specific server send initial messages to the clients
            // now that they've been told the game has started.
            if (controller != null) {
//                controller.sendInitialClientMessages(conn, tableNum);
            }
        }
    }

    /**
     * User sits down at a table.
     *
     * @param conn         Connection to client.
     * @param commSitDown  Message to sit down.
     * @param tableNum     Table number.
     * @param table        The table we're sitting at
     */
    private void sitDown(ServerConnectionThread conn, MsgSitDown commSitDown,
                         String tableNum, Table table) {
        // Get the Player
        PlayerList players = table.getPlayerList();
        Player player = players.getPlayer(conn.getUserId());
        if (player == null) {
            return;
        }

        int seatNum = commSitDown.getSeatNum();

        // Ensure that the seat is free
        if (players.isSeatFree(seatNum)) {
            player.setSeatNum(seatNum);
            player.sit();

            // Transmit to all users that this user has sat down
            MsgPlayerState commPlayerState = new MsgPlayerState(
                    tableNum, conn.getUserId(), seatNum);
            conn.transmitToTablePlayers(tableNum, commPlayerState);
            conn.sendDataMessageToAdmin(commPlayerState);
        }
    }
}
